package md.sv.dbu.config;

import md.sv.dbu.utils.DbuUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author MG
 */
public class XmlParserImpl implements XmlParser {
    private static final Logger logger = LoggerFactory.getLogger(XmlParserImpl.class);

    SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    XPathFactory xPathFactory = XPathFactory.newInstance();

    @Override
    public <T> T parseConfigFile(InputStream inputStream, Class<T> clazz, InputStream validationXsd) throws JAXBException, SAXException {
        // Set up JAXB unmarshaller
        JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

        // Setup xsd validation schema if a validation xsd is provided
        if (validationXsd != null) {
            Source source = new StreamSource(validationXsd);
            Schema schema = schemaFactory.newSchema(source);
            unmarshaller.setSchema(schema);
        }

        return unmarshaller.unmarshal(new StreamSource(inputStream), clazz).getValue();
    }

    @Override
    public void parseXmlDocument(Node parentNode, XmlParserCallback callback) {
        recursiveNodeRead(parentNode, "", callback);
    }

    private void recursiveNodeRead(Node node, String fullName, XmlParserCallback callback) {
        if (node.getChildNodes().getLength() > 0) {
            for (int i = 0; i < node.getChildNodes().getLength(); i++) {
                recursiveNodeRead(node.getChildNodes().item(i), fullName + node.getNodeName() + ".", callback);
            }
        } else {
            // is leaf node
            callback.setXmlValue(fullName + node.getNodeName(), node.getNodeValue());
        }
    }

    @Override
    public Node findNodeByValue(Node node, String value) {
        return recursiveNodeValueFind(node, value);
    }

    private Node recursiveNodeValueFind(Node node, String value) {
        if (node.getChildNodes().getLength() > 0) {
            for (int i = 0; i < node.getChildNodes().getLength(); i++) {
                Node foundNode = recursiveNodeValueFind(node.getChildNodes().item(i), value);
                if (foundNode != null) {
                    return foundNode;
                }
            }
        } else {
            // is leaf node
            if (node.getNodeValue().contains(value)) {
                return node;
            }
        }
        return null;
    }

    @Override
    public boolean hasChildNode(Node node, String nodeName) {
        Node childNode = recursiveNodeFind(node, nodeName);
        return childNode != null;
    }

    private Node recursiveNodeFind(Node node, String nodeName) {
        if (node.getChildNodes().getLength() > 0) {
            for (int i = 0; i < node.getChildNodes().getLength(); i++) {
                recursiveNodeFind(node.getChildNodes().item(i), nodeName);
            }
        } else {
            // is leaf node
            if (node.getNodeName().equals(nodeName)) {
                return node;
            }
        }
        return null;
    }


    @Override
    public boolean validateXml(String document, String xsd) {
        if (document != null && xsd != null) {
            try {
                Schema schema = schemaFactory.newSchema(new StreamSource(DbuUtils.getInputStream(xsd)));
                Validator validator = schema.newValidator();
                validator.validate(new StreamSource(DbuUtils.getInputStream(document)));
                return true;
            } catch (Exception e) {
                logger.debug("Validation of document against xsd failed: " +  e.getMessage());
            }
        }
        return false;
    }

    @Override
    public boolean validateXml(String document, String[] xsd) {
        if (document != null && xsd != null) {
            String lastErrMsg = null;
            while (true) {
                try {
                    validate(document, xsd);
                    return true;
                } catch (IOException ioe) {
                    logger.error("IO Exception when trying to validate xml: ", ioe);
                } catch (SAXException e) {
                    if ((lastErrMsg == null || !lastErrMsg.equals(e.getMessage()))
                         && e.getMessage().contains("to a(n) 'type definition' component.")) {
                        logger.debug("SAX parser could not resolve name: {} ... trying once again", e.getMessage());
                        lastErrMsg = e.getMessage();
                    } else {
                        logger.debug("Validation of document against xsd failed: " + e.getMessage());
                        return false;
                    }
                }
            }
        }
        return false;
    }

    private void validate(String document, String[] xsd) throws IOException, SAXException {
        StreamSource[] sources = new StreamSource[xsd.length];
        for (int i = 0; i < xsd.length; i++) {
            sources[i] = new StreamSource(DbuUtils.getInputStream(xsd[i]));
        }

        Schema schema = schemaFactory.newSchema(sources);
        Validator validator = schema.newValidator();
        validator.validate(new StreamSource(DbuUtils.getInputStream(document)));
    }
}
